package hikari.managers.undo;

import com.gooddies.wiring.Wiring;
import hikari.utils.Utils;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * You should not use this object. Please use only Undo.get().createPojoUndo(object);
 */
class PojoUndoable implements Undoable {

    private static Map<Class, GetterSetterPair> classGetterMap = new HashMap<>();
    private Object object;
    private GetterSetterPair getterSetterPair;
    private Object previousValue;

    @Override
    public boolean undo() {
        try {
            getterSetterPair.setter.invoke(object, previousValue);
            return true;
        } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) {
            throw new RuntimeException(ex);
        }
    }

    public PojoUndoable(Object object) {
        NameAndMethod method = getMethodName(4);
        getterSetterPair = getCorrespondingGetterSetter(method, object);
        this.object = object;
        try {
            previousValue = getterSetterPair.getter.invoke(object);
            if (previousValue instanceof Cloneable) {
                try {
                    previousValue = previousValue.getClass().getMethod("clone").invoke(previousValue);
                } catch (NoSuchMethodException | SecurityException ex) {
                    Logger.getLogger(PojoUndoable.class.getName()).log(Level.SEVERE, null, ex);
                }
            }
        } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) {
            Wiring.getComponent(Utils.class).logError(ex);
            throw new RuntimeException(ex);
        }
    }

    private GetterSetterPair getCorrespondingGetterSetter(NameAndMethod nameMethod, Object obj) {
        if (!nameMethod.method.startsWith("set")) {
            throw new RuntimeException("method " + nameMethod.method + " is not setter. Class " + obj.getClass().getName());
        }

        String method = nameMethod.method;
        String beanName = method.substring(3);
        GetterSetterPair pair = getGetterSetter(obj.getClass(), beanName);
        return pair;
    }

    private static GetterSetterPair getGetterSetter(Class clazz, String beanName) {
        if (classGetterMap.get(clazz) != null) {
            return classGetterMap.get(clazz);
        }
        Method getter = findMethodInClass("get" + beanName, clazz);
        if (getter == null) {
            getter = findMethodInClass("is" + beanName, clazz);
        }
        if (getter == null) {
            throw new RuntimeException("Class " + clazz.getName() + " does not have getter method for bean " + beanName);
        }

        Method setter = findMethodInClass("set" + beanName, clazz);
        if (setter == null) {
            throw new RuntimeException("Class " + clazz.getName() + " does not have setter method for bean " + beanName);
        }
        getter.setAccessible(true);
        setter.setAccessible(true);
        GetterSetterPair getterSetter = new GetterSetterPair();
        getterSetter.setter = setter;
        getterSetter.getter = getter;
        classGetterMap.put(clazz, getterSetter);
        return getterSetter;
    }

    private static Method findMethodInClass(String methodName, Class clazz) {
        Method m = null;
        while (clazz != null) {
            for (Method method : clazz.getDeclaredMethods()) {
                if (method.getName().equals(methodName)) {
                    if (m == null) {
                        m = method;
                    } else {
                        throw new RuntimeException("Class " + clazz.getCanonicalName() + " have several methods with name " + methodName);
                    }
                }
            }
            clazz = clazz.getSuperclass();
        }
        return m;
    }

    private static NameAndMethod getMethodName(final int depth) {
        final StackTraceElement[] ste = Thread.currentThread().getStackTrace();
        NameAndMethod nam = new NameAndMethod();
        nam.method = ste[depth].getMethodName();
        nam.name = ste[depth].getClassName();
        return nam;
    }

    private static class NameAndMethod {

        String name;
        String method;
    }

    private static class GetterSetterPair {

        Method getter;
        Method setter;
    }
}
